package tt.dz.egobus.servicemanager.core.lock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tt.dz.egobus.cache.service.ExtendCacheService;

import java.util.concurrent.ThreadLocalRandom;

/**
 * @author Anson
 * @date 17-8-7
 * @name 基于redis实现的分布式锁抽象类
 * @since 1.0.6
 */
public abstract class AbstractRedisCacheLock implements CacheLock {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRedisCacheLock.class);


    protected static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
    /**
     * 锁等待时间，防止线程饥饿
     */
    protected static final int TIMEOUT_MSECS = 10 * 1000;

    /**
     * 锁超时时间，防止线程在入锁以后，无限的执行等待
     */
    protected static final int EXPIRE_MSECS = 60 * 1000;


    private volatile boolean isLocked = false;


    protected abstract ExtendCacheService cacheService();

    protected abstract String getLockKey();

    @Override
    public boolean aquireLock() {
        ExtendCacheService extendCacheService = cacheService();
        String lockKey = this.getLockKey();
        ThreadLocalRandom random = ThreadLocalRandom.current();
        int timeout = this.getTimeoutMsecs();
        long expireMsecs = this.getExpireMsecs();
        this.isLocked = false;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            //锁到期时间
            String expiresStr = String.valueOf(expires);
            if (extendCacheService.setNX(lockKey, expiresStr)) {
                // lock acquired
                this.isLocked = true;
                break;
            }
            //redis里的时间
            String currentValueStr = extendCacheService.get(lockKey);
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                //判断是否为空，不为空的情况下，如果被其他线程设置了值，则第二个条件判断是过不去的
                // lock is expired
                String oldValueStr = extendCacheService.getSet(lockKey, expiresStr);
                //获取上一个锁到期时间，并设置现在的锁到期时间，
                //只有一个线程才能获取上一个线上的设置时间，因为jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    //防止误删（覆盖，因为key是相同的）了他人的锁——这里达不到效果，这里值会被覆盖，但是因为什么相差了很少的时间，所以可以接受
                    //[分布式的情况下]:如过这个时候，多个线程恰好都到了这里，但是只有一个线程的设置值和当前值相同，他才有权利获取锁
                    // lock acquired
                    this.isLocked = true;
                    break;
                }
            }
            long relay = random.nextLong(this.getDefaultAcquiryResolutionMillis());
            timeout -= relay;
                /*
                延迟100 毫秒,  这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
                只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
                使用随机的等待时间可以一定程度上保证公平性
                */
            try {
                Thread.sleep(relay);
            } catch (InterruptedException e) {
                LOGGER.error("redis分布式并发锁,睡眠延迟线程阻塞", e);
            }
        }
        return this.isLocked;
    }


    @Override
    public boolean unlock() {
        if (this.isLocked) {
            cacheService().del(this.getLockKey());
            this.isLocked = false;
        }
        return true;
    }


    protected int getDefaultAcquiryResolutionMillis() {
        return DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
    }

    public static int getTimeoutMsecs() {
        return TIMEOUT_MSECS;
    }

    public static int getExpireMsecs() {
        return EXPIRE_MSECS;
    }
}
